﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Data.Objects;
using System.Linq;
using System.Runtime.Serialization;
using SmartDatingService;

namespace SmartDatingService {
    public partial class ServicePrototype {
        [DataContract]
        private struct SearchInfo {
            [DataMember]
            public byte[] SessionKeyHash;
            [DataMember]
            public Guid ID;
            [DataMember]
            public Dictionary<string, string> Criterias;
        }

        // searchId --> SearchInfo
        [DataMember]
        private static Dictionary<Guid, SearchInfo> _searches = new Dictionary<Guid, SearchInfo>();
        // lock on the search data
        private static object _searchDataLock = new object();

        public ResultWithStatus<string> GetNeighborIds(string sessionKeyHash) {
            lock (_sessionDataLock) {
                OperationStatus skResult = checkSessionKey(sessionKeyHash);
                if (skResult != OperationStatus.Success)
                    return ResultWithStatus<string>.NewErrorResult(skResult);
            }

            Entities contextEntities = new Entities();
            try {
                Guid thisId = userIdByHash(contextEntities, sessionKeyHash);
                IEnumerable<Location> locs = (from loc in contextEntities.Locations
                                              where (loc.usr_id != thisId)
                                              select loc).ToList();
                // near distance = 1 km
                const double NEAR_DISTANCE = 1000.0;
                // locations in the near distance, distance ascending
                locs = (from loc in locs
                        where calcDistance(contextEntities, loc.usr_id, thisId) <= NEAR_DISTANCE
                        orderby calcDistance(contextEntities, loc.usr_id, thisId)
                        select loc);
                // get guids and join them with ,
                return ResultWithStatus<string>.NewSuccess(
                    String.Join(",",
                        (from loc in locs
                         select loc.usr_id.ToString()).ToArray()));
            } finally {
                contextEntities.Dispose();
            }
        }

        /// <summary>
        /// Gets personal data for another user in XML string format
        /// </summary>
        /// <param name="sessionKeyHash">session key hash</param>
        /// <param name="userId">another user's guid</param>
        /// <returns>XML representation of PersonalData, encrypted with session key</returns>
        public ResultWithStatus<string> GetForeignPersonalData(string sessionKeyHash, Guid userId)
        {
            byte[] sessionKey;
            lock (_sessionDataLock) {
                // Check whether session key hash and password are correct
                OperationStatus skCheck = checkSessionKey(sessionKeyHash);
                if (skCheck != OperationStatus.Success)
                    return ResultWithStatus<string>.NewErrorResult(skCheck);
                // Get session key to decrypt the password
                sessionKey = _activeSessionKeys[b(sessionKeyHash)];
            }

            Entities contextEntities = new Entities();
            try {
                PersonalData pdForUser = (from pd in contextEntities.PersonalDatas
                                          where (pd.usr_id == userId)
                                          select pd).FirstOrDefault();
                if (pdForUser == null) {
                    return ResultWithStatus<string>.NewErrorResult(OperationStatus.NoPersonalData);
                }

                return ResultWithStatus<string>.NewSuccess(s(CryptoImpl.
                    writeObjectEncrypted<PersonalData>(pdForUser, sessionKey)));
            } catch (Exception) {
                return ResultWithStatus<string>.NewErrorResult(OperationStatus.Failure);
            } finally {
                contextEntities.Dispose();
            }
        }

        public ResultWithStatus<Guid> CreateSearch(string sessionKeyHash) {
            OperationStatus skResult = checkSessionKey(sessionKeyHash);
            if (skResult != OperationStatus.Success)
                return ResultWithStatus<Guid>.NewErrorResult(skResult);

            lock (_searchDataLock) {
                Guid newSearchId = Guid.NewGuid();
                _searches.Add(newSearchId, new SearchInfo() {
                    ID = newSearchId,
                    Criterias = new Dictionary<string, string>(),
                    SessionKeyHash = b(sessionKeyHash)
                });
                return ResultWithStatus<Guid>.NewSuccess(newSearchId);
            }
        }

        public OperationStatus DestroySearch(string sessionKeyHash, Guid searchId) {
            OperationStatus skResult = checkSessionKey(sessionKeyHash);
            if (skResult != OperationStatus.Success)
                return skResult;
            lock (_searchDataLock) {
                if (!_searches.ContainsKey(searchId))
                    return OperationStatus.InvalidSearchDescriptor;
                if (!new ByteEqualityComparer().Equals(_searches[searchId].SessionKeyHash, b(sessionKeyHash)))
                    return OperationStatus.InvalidSearchDescriptor;

                _searches.Remove(searchId);
            }
            return OperationStatus.Success;
        }

        public OperationStatus AddSearchCriteria(string sessionKeyHash, Guid searchId, string criteria, string encryptedCriteriaValue) {
            OperationStatus skResult = checkSessionKey(sessionKeyHash);
            if (skResult != OperationStatus.Success)
                return skResult;
            byte[] sessionKey = sessionKeyByHash(sessionKeyHash);

            lock (_searchDataLock) {
                if (!_searches.ContainsKey(searchId))
                    return OperationStatus.InvalidSearchDescriptor;
                if (!new ByteEqualityComparer().Equals(_searches[searchId].SessionKeyHash, b(sessionKeyHash)))
                    return OperationStatus.InvalidSearchDescriptor;

                string value = Encoding.Unicode.GetString(CryptoImpl.decryptAes(b(encryptedCriteriaValue), sessionKey));
                if (_searches[searchId].Criterias.ContainsKey(criteria))
                    return OperationStatus.DuplicateSearchCriteria;

                _searches[searchId].Criterias.Add(criteria, value);
                return OperationStatus.Success;
            }
        }

        public OperationStatus RemoveSearchCriteria(string sessionKeyHash, Guid searchId, string criteria) {
            OperationStatus skResult = checkSessionKey(sessionKeyHash);
            if (skResult != OperationStatus.Success)
                return skResult;
            byte[] sessionKey = sessionKeyByHash(sessionKeyHash);

            lock (_searchDataLock) {
                if (!_searches.ContainsKey(searchId))
                    return OperationStatus.InvalidSearchDescriptor;
                if (!new ByteEqualityComparer().Equals(_searches[searchId].SessionKeyHash, b(sessionKeyHash)))
                    return OperationStatus.InvalidSearchDescriptor;
                if (!_searches[searchId].Criterias.ContainsKey(criteria))
                    return OperationStatus.SearchCriteriaNotFound;

                _searches[searchId].Criterias.Remove(criteria);
                return OperationStatus.Success;
            }
        }

        public OperationStatus ClearSearchCriterias(string sessionKeyHash, Guid searchId) {
            OperationStatus skResult = checkSessionKey(sessionKeyHash);
            if (skResult != OperationStatus.Success)
                return skResult;
            byte[] sessionKey = sessionKeyByHash(sessionKeyHash);

            lock (_searchDataLock) {
                if (!_searches.ContainsKey(searchId))
                    return OperationStatus.InvalidSearchDescriptor;
                if (!new ByteEqualityComparer().Equals(_searches[searchId].SessionKeyHash, b(sessionKeyHash)))
                    return OperationStatus.InvalidSearchDescriptor;

                _searches[searchId].Criterias.Clear();
                return OperationStatus.Success;
            }
        }

        /// <summary>
        /// Calculates distance between two users with specified IDs.
        /// Latitude & longitude is taken from DB, if information about at least 1 user
        /// is absent, distance is assumed to be decimal.MaxValue.
        /// </summary>
        private double calcDistance(Entities contextEntities, Guid oneUser, Guid anotherUser) {
            // get locations
            Location loc1 =
                (from loc in contextEntities.Locations
                where loc.usr_id == oneUser
                select loc).FirstOrDefault();
            Location loc2 =
                (from loc in contextEntities.Locations
                where loc.usr_id == anotherUser
                select loc).FirstOrDefault();

            // locations(s) not set ==> maximum distance
            if (loc1 == null || loc2 == null) {
                return double.MaxValue;
            }

            // latitude and longitude in degrees
            double llat1 = loc1.latitude;
            double llat2 = loc2.latitude;
            double llong1 = loc1.longitude;
            double llong2 = loc2.longitude;

            // lat and long in radians
            const double DEG_IN_RAD = Math.PI / 180; // 1 degree in radians
            double lat1 = llat1 * DEG_IN_RAD;
            double lat2 = llat2 * DEG_IN_RAD;
            double long1 = llong1 * DEG_IN_RAD;
            double long2 = llong2 * DEG_IN_RAD;

            // cosines and sines of latitudes, differences of longitudes
            double cl1 = Math.Cos(lat1);
            double cl2 = Math.Cos(lat2);
            double sl1 = Math.Sin(lat1);
            double sl2 = Math.Sin(lat2);
            double delta = long2 - long1;
            double cdelta = Math.Cos(delta);
            double sdelta = Math.Sin(delta);

            // length of the larger circle
            double y = Math.Sqrt(Math.Pow(cl2 * sdelta, 2) + 
                Math.Pow(cl1 * sl2 - sl1 * cl2 * cdelta, 2));
            double x = sl1 * sl2 + cl1 * cl2 * cdelta;
            double ad = Math.Atan2(y,x);

            const double EARTH_RADIUS = 6372795.0;
            double dist = ad * EARTH_RADIUS;

            return dist;
        }

        public ResultWithStatus<SearchResult[]> GetSearchResults(string sessionKeyHash, Guid searchId) {
            OperationStatus skResult = checkSessionKey(sessionKeyHash);
            if (skResult != OperationStatus.Success)
                return ResultWithStatus<SearchResult[]>.NewErrorResult(skResult);
            byte[] sessionKey = sessionKeyByHash(sessionKeyHash);

            Dictionary<string, string> searchCriterias;
            lock (_searchDataLock) {
                if (!_searches.ContainsKey(searchId))
                    return ResultWithStatus<SearchResult[]>.
                        NewErrorResult(OperationStatus.InvalidSearchDescriptor);
                if (!new ByteEqualityComparer().Equals(_searches[searchId].SessionKeyHash, b(sessionKeyHash)))
                    return ResultWithStatus<SearchResult[]>.
                        NewErrorResult(OperationStatus.InvalidSearchDescriptor);

                searchCriterias = new Dictionary<string, string>(_searches[searchId].Criterias);
                _searches.Remove(searchId);
            }

            if (searchCriterias.Count == 0)
                return ResultWithStatus<SearchResult[]>.
                    NewErrorResult(OperationStatus.NoCriteriasDefined);

            List<string> RECOGNIZED_CRITERIAS = new List<string>(new string[]
            {"gender", "age", "u_weight", "u_height", "eye_color", "hair_color", "main_size",
            "chest", "waist", "hip", "orientation", "zodiac", "reason", "interests", "bio",
            "name"});
            Entities contextEntities = new Entities();

            ObjectQuery<PersonalData> memberships = contextEntities.PersonalDatas;
            int currentYear = DateTime.UtcNow.Year;
            foreach (KeyValuePair<string, string> kvp in searchCriterias) {
                string paramName = kvp.Key.ToLower();
                if (RECOGNIZED_CRITERIAS.Contains(paramName)) {
                    if (paramName == "age") {
                        memberships = memberships.Where("(" + currentYear.ToString() + " - YEAR(it.date_of_birth)) >= @" + paramName + "",
                            new ObjectParameter(paramName, int.Parse(kvp.Value)));
                    } else if (paramName == "interests" || paramName == "bio") {
                        memberships = memberships.Where("it." + paramName + " like @" + paramName + "",
                            new ObjectParameter(paramName, "%" + kvp.Value + "%"));
                    } else if (paramName == "name") {
                        memberships = memberships.Where("it.first_name like @" + paramName + " "+
                        "or it.last_name like @" + paramName + "",
                            new ObjectParameter(paramName, "%" + kvp.Value + "%"));
                    } else {
                        memberships = memberships.Where("it." + paramName + " = @" + paramName,
                            new ObjectParameter(paramName, int.Parse(kvp.Value)));
                    }
                } else {
                    return ResultWithStatus<SearchResult[]>.
                        NewErrorResult(OperationStatus.UnknownSearchCriteria);
                }
            }

            List<SearchResult> srList = new List<SearchResult>();
            Guid thisUserId = userIdByHash(contextEntities, sessionKeyHash);
            foreach (PersonalData member in memberships) {
                if (member.usr_id != thisUserId) {
                    IEnumerable<PersonalData> pdForMembers =
                        from pd in contextEntities.PersonalDatas
                        where (pd.usr_id == member.usr_id)
                        select pd;
                    Location latlng =
                        (from loc in contextEntities.Locations
                        where (loc.usr_id == member.usr_id)
                        select loc).FirstOrDefault();

                    if (pdForMembers.Count() > 0) {
                        srList.Add(new SearchResult() {
                            pd = pdForMembers.First(),
                            latitude = (latlng == null ? (double?)null : latlng.latitude),
                            longitude = (latlng == null ? (double?)null : latlng.longitude)
                        });
                    }
                }
            }

            srList = (from sr in srList
                     orderby calcDistance(contextEntities, sr.pd.usr_id, thisUserId) ascending
                     select sr).ToList();

            if (srList.Count == 0) {
                return ResultWithStatus<SearchResult[]>.
                    NewErrorResult(OperationStatus.NoMembersFound);
            }
            return ResultWithStatus<SearchResult[]>.NewSuccess(srList.ToArray());
        }
    }
}